import java.io.File;
import java.util.TreeSet;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class Parser {

	private HashMap<String, Object> variables;

	//We try to match
	private DFA keywordDFA, tokenDFA;

	private enum state {KEYWORD, TOKEN};

	private class Token{
		public String name, value;
		public Token(String name, String value){
			this.name = name;
			this.value = value;
		}

		public String toString(){
			return "<" + name + ", " + value + ">";
		}
	}

	public static void main(String[] args){
		Parser p = new Parser();
		//System.out.println(p.scanFile("testingmini.txt"));
		/*System.out.println(maxfreqstring("abc def ghi asdfo;iwjef abc def"));
		System.out.println(recursivereplace("[a-z]+", "?", "File1", "File2"));
		System.out.println(replace("[a-z]+", "}", "File1", "File3"));*/
		//replace("a", "}", "File1", "File4");
		try{
			p.parseFile(p.scanFile("basicTest"));
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}

	public boolean parseFile(ArrayList<ArrayList<Token>> tokens) throws Exception
	{
		ArrayList<Token> currentline=null;
		//check begin and end		
		if(!tokens.get(0).get(0).value.equals("begin")){
			throw new Exception("Parsing Error in line" + 0 + "\n");
		}
		if(!tokens.get(tokens.size()-1).get(0).value.equals("end")){
			throw new Exception("Missing \"end\".");
		}
		//parse statementes
		boolean valid;
		for (int i=1; i<tokens.size()-1;i++)
		{
			valid = false;
			currentline=tokens.get(i);
			//System.out.println("Line " + i + ": " + currentline);

			//Make sure there's a semicolon
			if(currentline.get((currentline.size()-1)).value.equals(";")){
				currentline.remove((currentline.size()-1));
			} 
			else {
				//System.out.println(currentline.get(currentline.size()-1).value);
				throw new Exception("Missing semicolon at line " + i + ".");
			}	

			if (currentline.get(0).name.equals("VAR"))
			{
				if(currentline.get(1).value.equals("=")){
					if(currentline.get(2).value.equals("maxfreqstring")){
						if(currentline.get(3).value.equals("(")){
							if(currentline.get(4).name.equals("VAR")){
								if(currentline.get(5).value.equals(")")){
									//assign maxfreqstring to var;
									valid = true;
									try{
										assign(currentline.get(0).value, maxfreqstring((StringList)variables.get(currentline.get(4).value)));
									}
									catch (Exception e){
										throw new Exception("Parsing error at line " + i + ", invalid maxfreqstring argument");
									}
								}	
							}	
						}	
					}
					else if (currentline.get(2).value.equals("#"))
					{
						ArrayList<Token> evalexpinput = new ArrayList<Token>();
						for (int j=3; j<currentline.size(); j++)
						{
							evalexpinput.add(currentline.get(j));
						}
						//assign length of evalexp to var;
						valid = true;
						assign(currentline.get(0).value, StringList.pound((StringList)evaluateExp(evalexpinput)));
					}
					else {
						ArrayList<Token> evalexpinput = new ArrayList<Token>();
						for (int j=2; j<currentline.size(); j++)
						{
							evalexpinput.add(currentline.get(j));
						}
						//assign evalexp to var;
						valid = true;
						//System.out.println(evalexpinput);
						assign(currentline.get(0).value, evaluateExp(evalexpinput));
					}		
				}		
			}

			else if (currentline.get(0).value.equals("replace"))
			{
				if (currentline.get(1).name.equals("REGEX")){
					if (currentline.get(2).value.equals("with")){
						if (currentline.get(3).name.equals("ASCIISTR")){
							if (currentline.get(4).value.equals("in")){
								if (currentline.get(5).name.equals("ASCIISTR")){
									if (currentline.get(6).value.equals(">!")){
										if (currentline.get(7).name.equals("ASCIISTR")){
											if (currentline.get(5).value.equals(currentline.get(7).value)){
												//source and destination can't be the same
												throw new Exception("Source and destination files can't be the same.");
											}
											else{
												valid = true;
												replace(currentline.get(1).value.substring(1,currentline.get(1).value.length()-1), currentline.get(3).value.substring(1,currentline.get(3).value.length()-1), currentline.get(5).value.substring(1,currentline.get(5).value.length()-1), currentline.get(7).value.substring(1,currentline.get(7).value.length()-1));
											}
										}
									}
									/*if (currentline.get(6).value == ";"){
										//call replace with filename being 1 string;
									}
									else if (currentline.get(6).value=="!"){
										if (currentline.get(7).name == "ASCIISTR"){
											if (currentline.get(8).value == ";"){
												//call replace with 2 filename strings
											}
											else {
												System.out.println("Parsing Error in line" + i + "\n");
												return false;					
											}
										}
										else {
											System.out.println("Parsing Error in line" + i + "\n");
											return false;					
										}*/
								}
							}
						}
					}
				}
			}

			else if(currentline.get(0).value.equals("recursivereplace"))
			{
				if (currentline.get(1).name.equals("REGEX")){
					if (currentline.get(2).value.equals("with")){
						if (currentline.get(3).name.equals("ASCIISTR")){
							if (currentline.get(4).value.equals("in")){
								if (currentline.get(5).name.equals("ASCIISTR")){
									if (currentline.get(6).value.equals(";")){

										//call recursivereplace with filename being 1 string;
									}
									else if (currentline.get(6).value.equals(">!")){
										if (currentline.get(7).name.equals("ASCIISTR")){
											if (currentline.get(5).value.equals(currentline.get(7).value)){
												throw new Exception("Source and destination files can't be the same.");
											}
											else{
												//call recursivereplace with 2 filename strings
												valid = true;
												recursivereplace(currentline.get(1).value.substring(1,currentline.get(1).value.length()-1), currentline.get(3).value.substring(1,currentline.get(3).value.length()-1), currentline.get(5).value.substring(1,currentline.get(5).value.length()-1), currentline.get(7).value.substring(1,currentline.get(7).value.length()-1));
											}
										}
									}
								}
							}
						}

					}
				}
			}

			else if(currentline.get(0).value.equals("print"))
			{
				currentline.remove(0);
				if(currentline.get(0).value.equals("("))
				{
					currentline.remove(0);
					if(currentline.get((currentline.size()-1)).value.equals(")")){
						currentline.remove((currentline.size()-1));
					}
					else{
						throw new Exception("Parsing Error in line" + i + "\n");
					}

					ArrayList printinput = new ArrayList();
					while(findComma(currentline)!=-1)
					{
						//System.out.println("curline " + currentline);
						/*while (findComma(currentline) == 0){
							currentline.remove(0);
						}*/
						ArrayList<Token> evalexpinput = new ArrayList<Token>();
						for (int j=0; j<findComma(currentline); j++)
						{
							evalexpinput.add(currentline.get(j));
						}
						//System.out.println("COMMA " + findComma(currentline));
						if(evaluateExp(evalexpinput)==null){
							throw new Exception("Parsing Error in line" + i + "\n");
						}
						else {
							printinput.add((StringList)evaluateExp(evalexpinput));
						}
						int stopcounter=findComma(currentline);
						for (int j=0; j<=stopcounter; j++)
						{
							currentline.remove(0);
						}
					}
					ArrayList<Token> evalexpinput = new ArrayList<Token>();
					for (int j=0; j<currentline.size(); j++)
					{
						evalexpinput.add(currentline.get(j));
					}
					if(evaluateExp(evalexpinput)==null){
						throw new Exception("Parsing Error in line" + i + "\n");
					}
					else{
						Object o = evaluateExp(evalexpinput);
						/*if (o instanceof StringList){
							printinput.add((StringList)o);
						}
						else{
							printinput.add((Integer)o);
						}*/
						printinput.add(o);
					}
					valid = true;
					printExpList(printinput);
				}
			}

			if (!valid){
				throw new Exception("Parse error at line " + i + ".");
			}
		}
		return true;
	}

	public int findComma(ArrayList<Token> explist)
	{
		int index=-1;
		for (int i=0; i<explist.size();i++)
		{
			if(explist.get(i).value.equals(",")){
				index=i;
				break;
			}
		}
		return index;
	}

	public Object evaluateExp(ArrayList<Token> exp)
	{
		if(exp.get(0).name.equals("VAR")){
			return variables.get(exp.get(0).value);//return value of var;
		}
		else if (exp.get(0).value.equals("find"))
		{
			return evaluateTerm(exp);
		}
		else if (exp.get(0).value.equals("("))
		{
			if(exp.get((exp.size()-1)).value.equals(")")){
				exp.remove(0);
				exp.remove(exp.size()-1);
				return evaluateExp(exp);
			}
			else {
				return null;
			}
		}
		else {
			return null;
		}
	}

	public StringList evaluateTerm(ArrayList<Token> exp){
		if (exp.size()==4){
			return find(exp.get(1).value.substring(1,exp.get(1).value.length()-1),exp.get(3).value.substring(1,exp.get(3).value.length()-1));
		} else if (exp.size()>4)
		{
			if(exp.get(exp.size()-5).value.equals("union")){
				ArrayList<Token> temp1= new ArrayList<Token>();
				ArrayList<Token> temp2= new ArrayList<Token>();
				for (int i=exp.size()-4;i<exp.size();i++)
				{
					temp2.add(exp.get(i));
				}
				for (int i=0; i<exp.size()-5;i++)
				{
					temp1.add(exp.get(i));
				}
				return StringList.union(evaluateTerm(temp1), find(temp2.get(1).value.substring(1,temp2.get(1).value.length()-1),temp2.get(3).value.substring(1,temp2.get(3).value.length()-1))); 
			}
			else if(exp.get(exp.size()-5).value.equals("diff")){
				ArrayList<Token> temp1= new ArrayList<Token>();
				ArrayList<Token> temp2= new ArrayList<Token>();
				for (int i=exp.size()-4;i<exp.size();i++)
				{
					temp2.add(exp.get(i));
				}
				for (int i=0; i<exp.size()-5;i++)
				{
					temp1.add(exp.get(i));
				}
				return StringList.diff(evaluateTerm(temp1), find(temp2.get(1).value.substring(1,temp2.get(1).value.length()-1),temp2.get(3).value.substring(1,temp2.get(3).value.length()-1))); //replace null with find result of temp2				
			}
			else if(exp.get(exp.size()-5).value.equals("inters"))
			{
				ArrayList<Token> temp1= new ArrayList<Token>();
				ArrayList<Token> temp2= new ArrayList<Token>();
				for (int i=exp.size()-4;i<exp.size();i++)
				{
					temp2.add(exp.get(i));
				}
				for (int i=0; i<exp.size()-5;i++)
				{
					temp1.add(exp.get(i));
				}
				return StringList.inters(evaluateTerm(temp1), find(temp2.get(1).value.substring(1,temp2.get(1).value.length()-1),temp2.get(3).value.substring(1,temp2.get(3).value.length()-1))); //replace null with find result of temp2
			}			
			else {return null;}
		} else {
			return null;
		}
	}

	public void printExpList(ArrayList explist)
	{
		for (int i=0; i<explist.size();i++)
		{
			System.out.println(explist.get(i).toString());
		}
	}


	public ArrayList<ArrayList<Token>> scanFile(String fn){
		ArrayList<ArrayList<Token>> tokens = new ArrayList<ArrayList<Token>>();
		String curline;
		try{
			Scanner scan = new Scanner(new File(fn));
			while(scan.hasNext()){
				curline = scan.nextLine();
				tokens.add(tokenize(curline));
			}
		}
		catch(Exception e){
			e.printStackTrace();
		}
		return tokens;
	}

	//Result will look like [<"abc", [File1.txt = {(4,2), (5,17)}, File2.txt = {(1,2)}]> <"def", {}>]
	// to indicate "abc" found on line 4, index 2 and line 5, index 17 in File1.txt, and
	// line 1, index 2 in File2.txt, and "def" has no occurrences in File2.txt
	public static StringList find(String regex, String fn){
		StringList sl = new StringList();
		try{
			Scanner scan = new Scanner(new File(fn));
			String curline;
			Pattern p = Pattern.compile(regex);
			Matcher m;
			int start, end;
			sl = new StringList();
			int lineNum = 0;
			while (scan.hasNext()){
				lineNum++;
				curline = scan.nextLine();
				m = p.matcher(curline);
				while (m.find()){

					start = m.start();
					end = m.end();
					sl.add(new StringListElement(curline.substring(start, end), fn, lineNum, start, end));
				}				
			}
		}
		catch(Exception e){
			e.printStackTrace();
		}
		return sl;
	}

	public static boolean replace(String regex, String newval, String fn_in, String fn_out){
		try{
			//System.out.println("Calling replace " + regex + " with " + newval + " in " + fn_in + " >! " + fn_out + " ;");
			Scanner scan = new Scanner(new File(fn_in));
			FileWriter fw = new FileWriter(new File(fn_out));
			String curline;
			while (scan.hasNext()){
				curline = scan.nextLine();
				curline = curline.replaceAll(regex, newval);
				//System.out.println("replace writing " + curline);

				fw.write(curline + "\n");
			}
			fw.close();
			return true;
		}
		catch(Exception e){
			return false;
		}
	}

	public static boolean recursivereplace(String regex, String newval, String fn_in, String fn_out){
		try{
			Scanner scan = new Scanner(new File(fn_in));
			FileWriter fw = new FileWriter(new File(fn_out));
			String curline;
			String oldline;
			char[] oldlineArray;
			boolean same;
			while (scan.hasNext()){
				int numRecursions = 0;
				same = true;
				curline = scan.nextLine();
				String[] tokens = curline.split("\\s");
				String newline = "";
				//String curtoken, oldtoken;
				int maxlen = curline.length();

				do {
					if (numRecursions++ > maxlen){//infinite recursion
						fw.close();
						return false;
					}
					//System.out.println(curline);
					//oldlineArray = curline.toCharArray();
					oldline = curline;//String.copyValueOf(oldlineArray);
					curline = curline.replaceAll(regex, newval);
					//System.out.println(curline);
				} while (!oldline.equals(curline));
				fw.write(curline + "\n");
			}
			fw.close();
		}
		catch(Exception e){
			e.printStackTrace();
			return false;
		}
		return true;
	}

	void assign(String varname, Object val){
		//System.out.println(varname + " " + val);
		variables.put(varname, val);
	}

	public static StringList maxfreqstring(StringList line){
		try{
			StringList sl = new StringList();
			int curmax = line.vals.get(0).info.size();
			int maxi = 0;
			for (int i = 1; i < line.vals.size(); i++){
				if (line.vals.get(i).info.size() > curmax){
					curmax = line.vals.get(i).info.size();
					maxi = i;
				}
			}
			sl.add(line.vals.get(maxi));
			return sl;
		}
		catch(Exception e){
			return new StringList();
		}
		/*HashMap<String, Integer> numstrings = new HashMap<String, Integer>();
		//String[] strings = line.split(" ");
		StringList sl = new StringList();
		for (int i = 0; i < line.vals.size(); i++){
			if (numstrings.containsKey(strings[i])){
				numstrings.put(strings[i], numstrings.get(strings[i]) + 1);
			}
			else{
				numstrings.put(strings[i], 0);
			}
		}
		//ArrayList<String> ret = new ArrayList<String>();
		int highest = 0;
		String val = null;
		for (String str : numstrings.keySet()){
			if (numstrings.get(str) > highest){
				val = str;
				highest = numstrings.get(str);
			}
		}
		sl.add(new StringListElement());
		sl.add(val);
		return ret;*/
	}


	public ArrayList<Token> tokenize(String line){
		ArrayList<Token> tokens = new ArrayList<Token>();

		int[] icount = new int[line.length()+1];
		for (int i = 0; i < icount.length; i++)
			icount[i] = 0;

		char[] in = line.toCharArray();
		DFANode curnode = keywordDFA.start;
		char c = 0;
		int tokenStart = 0;
		boolean failed = false;
		state curstate = state.KEYWORD;
		for (int i = 0; i <= in.length; i++){
			if (i > in.length || icount[i] >= 40){
				tokens.clear();
				tokens.add(new Token("Failed at character " + i + ".", ""));
				return tokens;
			}
			icount[i] = icount[i] + 1;

			/*switch (curstate) {
			case KEYWORD:*/

			//Try to match a keyword first
			try{
				c = in[i];
				if (curnode.adjList.get(c) != null){//we have a transition to make
					curnode = (DFANode)curnode.adjList.get(c).get(0);
					failed = false;
				}
				else{//We have nowhere to go - figure out if it's a valid token and we can go on
					if (DFA.isInAlphabet(c)){//if it's a printable ascii character, try non-keywords
						/*ret.clear();
							ret.add("Failed at character " + i + ".");
							return ret;*/
						i = tokenStart;
						curstate = state.TOKEN;
					}
					else{//we have whitespace of some sort
						if (curnode.accepting){
							tokens.add(new Token(curnode.tokenName, line.substring(tokenStart, i)));
						}
						else{//we ended on a non-token
							/*ret.clear();
							ret.add("Failed at character " + i + ".");
							return ret;*/
							i = tokenStart;
							curstate = state.TOKEN;
						}
					}
					if (curstate != state.TOKEN){
						while (!DFA.isInAlphabet(c)){

							if (++i >= in.length){
								return tokens;
							}
							c = in[i];
						}
						i--;
						tokenStart = i+1;
						curnode = keywordDFA.start;
					}
				}
			}
			catch(Exception e){
				if (i >= in.length && curnode.accepting){
					tokens.add(new Token(curnode.tokenName, line.substring(tokenStart, i)));
					return tokens;
				}
				else{
					/*ret.clear();
					ret.add("Failed at character " + i + ".");
					return ret;*/
					i = tokenStart;
					curstate = state.TOKEN;
				}

			}
			//break;

			//case TOKEN:
			if (curstate == state.TOKEN){
				curstate = state.KEYWORD;//reset every time
				curnode = tokenDFA.start;//start looking for tokens
				try{
					c = in[i];
					if (curnode.adjList.get(c) != null){//we have a transition to make
						curnode = (DFANode)curnode.adjList.get(c).get(0);
						failed = false;
					}
					else{//We have nowhere to go - figure out if it's a valid token and we can go on
						if (DFA.isInAlphabet(c)){// && !curnode.accepting){//if it's a printable ascii character, try non-keywords
							tokens.clear();
							tokens.add(new Token("Failed at character " + i + ".", ""));
							return tokens;
						}
						/*else if (DFA.isInAlphabet(c)){
							tokens.add(new Token(curnode.tokenName, line.substring(tokenStart, i)));
							i--;
						}*/
						else{//we have whitespace of some sort
							if (curnode.accepting){
								tokens.add(new Token(curnode.tokenName, line.substring(tokenStart, i)));
							}
							else{//we ended on a non-token
								tokens.clear();
								tokens.add(new Token("Failed at character " + i + ".", ""));
								return tokens;
							}
						}
						while (!DFA.isInAlphabet(c)){

							if (++i >= in.length){
								return tokens;
							}
							c = in[i];
						}
						i--;
						tokenStart = i+1;
						curnode = keywordDFA.start;
					}
				}
				catch(Exception e){
					if (i >= in.length && curnode.accepting){
						tokens.add(new Token(curnode.tokenName, line.substring(tokenStart, i)));
						return tokens;
					}
					else{
						tokens.clear();
						tokens.add(new Token("Failed at character " + i + ".", ""));
						return tokens;
					}
				}			
			}
		}

		return tokens;
	}

	public Parser(){
		variables = new HashMap<String, Object>();
		String tokenFN = "MiniRETokens.txt";
		String keywordFN = "MiniREKeywords.txt";
		try{
			Lexer lex = new Lexer(tokenFN);
			lex.dfa.save(tokenFN);
			tokenDFA = lex.dfa;
			Lexer lex2 = new Lexer(keywordFN);
			lex2.dfa.save(keywordFN);
			keywordDFA = lex2.dfa;
		}
		catch(Exception e){
			e.printStackTrace();
		}
		//System.out.println("creation done");
	}
}
